// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Xunit;

namespace Microsoft.AspNetCore.DataProtection
{
    /// <summary>
    /// Helpful XML-based assertions.
    /// </summary>
    public static class XmlAssert
    {
        public static readonly IEqualityComparer<XNode> EqualityComparer = new CallbackBasedEqualityComparer<XNode>(Core.AreEqual);

        /// <summary>
        /// Asserts that a <see cref="string"/> and an <see cref="XElement"/> are semantically equivalent.
        /// </summary>
        public static void Equal(string expected, XElement actual)
        {
            Assert.NotNull(expected);
            Assert.NotNull(actual);
            Equal(XElement.Parse(expected), actual);
        }

        /// <summary>
        /// Asserts that two <see cref="XElement"/> instances are semantically equivalent.
        /// </summary>
        public static void Equal(XElement expected, XElement actual)
        {
            Assert.NotNull(expected);
            Assert.NotNull(actual);

            if (!Core.AreEqual(expected, actual))
            {
                Assert.True(false,
                       "Expected element:" + Environment.NewLine
                       + expected.ToString() + Environment.NewLine
                       + "Actual element:" + Environment.NewLine
                       + actual.ToString());
            }
        }

        private static class Core
        {
            private static readonly IEqualityComparer<XAttribute> AttributeEqualityComparer = new CallbackBasedEqualityComparer<XAttribute>(AreEqual);

            private static bool AreEqual(XElement expected, XElement actual)
            {
                return expected.Name == actual.Name
                    && AreEqual(expected.Attributes(), actual.Attributes())
                    && AreEqual(expected.Nodes(), actual.Nodes());
            }

            private static bool AreEqual(IEnumerable<XNode> expected, IEnumerable<XNode> actual)
            {
                List<XNode> filteredExpected = expected.Where(ShouldIncludeNodeDuringComparison).ToList();
                List<XNode> filteredActual = actual.Where(ShouldIncludeNodeDuringComparison).ToList();
                return filteredExpected.SequenceEqual(filteredActual, EqualityComparer);
            }

            internal static bool AreEqual(XNode expected, XNode actual)
            {
                if (expected is XText && actual is XText)
                {
                    return AreEqual((XText)expected, (XText)actual);
                }
                else if (expected is XElement && actual is XElement)
                {
                    return AreEqual((XElement)expected, (XElement)actual);
                }
                else
                {
                    return false;
                }
            }

            private static bool AreEqual(XText expected, XText actual)
            {
                return expected.Value == actual.Value;
            }

            private static bool AreEqual(IEnumerable<XAttribute> expected, IEnumerable<XAttribute> actual)
            {
                List<XAttribute> orderedExpected = expected
                    .Where(ShouldIncludeAttributeDuringComparison)
                    .OrderBy(attr => attr.Name.ToString())
                    .ToList();

                List<XAttribute> orderedActual = actual
                    .Where(ShouldIncludeAttributeDuringComparison)
                    .OrderBy(attr => attr.Name.ToString())
                    .ToList();

                return orderedExpected.SequenceEqual(orderedActual, AttributeEqualityComparer);
            }

            private static bool AreEqual(XAttribute expected, XAttribute actual)
            {
                return expected.Name == actual.Name
                    && expected.Value == actual.Value;
            }

            private static bool ShouldIncludeAttributeDuringComparison(XAttribute attribute)
            {
                // exclude 'xmlns' attributes since they're already considered in the
                // actual element and attribute names
                return attribute.Name != (XName)"xmlns"
                    && attribute.Name.Namespace != XNamespace.Xmlns;
            }

            private static bool ShouldIncludeNodeDuringComparison(XNode node)
            {
                if (node is XComment)
                {
                    return false; // not contextually relevant
                }

                if (node is XText /* includes XCData */ || node is XElement)
                {
                    return true; // relevant
                }

                throw new NotSupportedException(string.Format("Node of type '{0}' is not supported.", node.GetType().Name));
            }
        }

        private sealed class CallbackBasedEqualityComparer<T> : IEqualityComparer<T>
        {
            private readonly Func<T, T, bool> _equalityCheck;

            public CallbackBasedEqualityComparer(Func<T, T, bool> equalityCheck)
            {
                _equalityCheck = equalityCheck;
            }

            public bool Equals(T x, T y)
            {
                return _equalityCheck(x, y);
            }

            public int GetHashCode(T obj)
            {
                return obj.ToString().GetHashCode();
            }
        }
    }
}
